# -*- coding: utf-8 -*-

"""
 (c) 2023 - Copyright CTyunOS Inc

 Authors:
   youyifeng <youyf2@chinatelecom.cn>

"""

import json
import logging

import requests
from cve_ease.helper import data_to_csv, notifier_timestamp
from cve_ease.models import CVE, SA

from .base import NotifierBase

logger = logging.getLogger('cve-ease')


class Wecom(NotifierBase):
    """
    企业微信群聊机器人
    官方文档: https://developer.work.weixin.qq.com/document/path/91770
    注意: API接口限制，每个机器人发送的消息不能超过20条/分钟，大概3s一条即可
    """

    def __init__(self, gconfig):
        self._name = 'wecom_notifier'
        self._config = gconfig
        # setting wanip
        if not hasattr(gconfig, 'WANIP'):
            from cve_ease.helper import get_wanip
            self._wanip = get_wanip()
        else:
            self._wanip = gconfig.WANIP
        # setting watcher
        self._watcher = None
        if hasattr(gconfig, 'WATCHER'):
            self._watcher = gconfig.WATCHER
        # setting notify timestamp
        self._notify_time = notifier_timestamp()

    def do_send_file(self, data, key: str, filename="sample.txt"):
        """
        企业微信发送文件接口
        要求文件大小在5B~20M之间
        :param data:
        :return:
        """
        headers = {
            'Content-Type': 'multipart/form-data',
        }
        files = {
            'file': (filename, data),
        }
        res = None
        # 申请/使用说明参考: https://developer.work.weixin.qq.com/document/path/91770
        # 样例接口: https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=KEY&type=TYPE
        url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={key}&type=file'
        r = requests.post(url=url, headers=headers, files=files)
        try:
            res = json.loads(r.text)
        except:
            pass
        if r.status_code == 200 and res and 'errcode' in res and 0 == res['errcode']:
            logger.debug(f'[+] {self._name} {key} send file success!')
            try:
                res = json.loads(r.text)
            except Exception as e:
                logger.error("json loads failed!", str(e))
                return ""
            if "media_id" in res and '' != res["media_id"]:
                return res["media_id"]
        else:
            logger.error("url:\n" + url)
            logger.error("response:\n" + r.text)
            logger.error(f'[-] {self._name} {key} send file failed!')
        return ""

    def do_send(self, data, key: str):
        res = None
        headers = {'Content-Type': 'application/json'}
        # 申请/使用说明参考: https://developer.work.weixin.qq.com/document/path/91770
        url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}'
        r = requests.post(url=url, headers=headers, data=json.dumps(data))
        try:
            res = json.loads(r.text)
        except:
            pass
        if r.status_code == 200 and res and 'errcode' in res and 0 == res['errcode']:
            logger.debug(f'[+] {self._name} {key} send mesg success!')
        else:
            logger.error("url:\n" + url)
            logger.error("request:\n" + json.dumps(data, indent=4))
            logger.error("response:\n" + r.text)
            logger.error(f'[-] {self._name} {key} send mesg failed!')

    def send_markdown(self, msg, key: str):
        data = {
            "msgtype": "markdown",
            "markdown": {
                "content": msg,
            },
        }
        self.do_send(data, key)

    def send_text(self, msg="", mentioned_mobile_list=[], key=None, debug=False):
        data = {
            "msgtype": "text",
            "text": {
                "content": msg,
                "mentioned_list": [],
                "mentioned_mobile_list": mentioned_mobile_list,
            }
        }
        self.do_send(data, key)

    def send_news(self, msg):
        data = {
            "msgtype": "news",
            "news": {
                "articles": [
                    {
                        "title": msg["title"] if "title" in msg else '',
                        "description": msg["description"] if "description" in msg else '',
                        "url": msg["url"] if "url" in msg else '',
                        "picurl": msg["picurl"] if "picurl" in msg else '',
                    }
                ]
            }
        }
        self.do_send(data)

    def send_image(self, msg):
        """图片（base64编码前）最大不能超过2M，支持JPG,PNG格式"""
        data = {
            "msgtype": "image",
            "image": {
                "base64": msg["base64"] if "base64" in msg else '',
                "md5": msg["md5"] if "md5" in msg else '',
            }
        }
        self.do_send(data)

    def send_file(self, msg, key=None, filename="sample.txt"):
        """图片（base64编码前）最大不能超过2M，支持JPG,PNG格式"""
        data = {
            "msgtype": "file",
            "file": {
                "media_id": self.do_send_file(msg, key, filename)
            }
        }
        if data["file"]["media_id"]:
            self.do_send(data, key)
        else:
            logger.error("upload file failed! no media_id found.")

    def get_info_column(self, info_type: str):
        """
        获取信息字段，用于导出csv
        :param info_type:
        :return:
        """
        if "cve" == info_type:
            return CVE.COLUMN
        elif "sa" == info_type:
            return SA.COLUMN
        else:
            raise f" no column for {info_type} "

    def do_update_notify(self, info_diff: dict, info_type: str):
        """
        更新播报
        :param info_diff:
        :param info_type:
        :return:
        """
        for w in self._watcher:
            cw = self._watcher[w]
            if self._name not in cw:
                continue
            if len(cw[self._name]) < 1 and '' == cw[self._name][0]:
                continue

            match = {}
            notifier_token = cw[self._name][0]
            flag = False

            filter_pkglist = cw['watch_pkg'] if 'watch_pkg' in cw else []
            if 'ALL' in filter_pkglist:
                match = info_diff
                flag = True
            filter_score = cw['watch_score'] if 'watch_score' in cw else 0.0
            if filter_score <= 0.0:
                match = info_diff
                flag = True

            if not match:
                for status in info_diff:
                    if status not in match:
                        match[status] = []
                    for index, cve in enumerate(filter_diff[status]):
                        if "packageName" in cve and "" != cve["packageName"]:
                            for fpkg in filter_pkglist:
                                if fpkg in filter_pkglist:
                                    flag = True
                                    match[status].append(cve)
                        elif "cvsssCoreNVD" in cve \
                                and "" != cve["cvsssCoreNVD"] \
                                and float(cve["cvsssCoreNVD"]) >= filter_score:
                            flag = True
                            match[status].append(cve)
                        elif "cvsssCoreOE" in cve \
                                and "" != cve["cvsssCoreOE"] \
                                and float(cve["cvsssCoreOE"]) >= filter_score:
                            flag = True
                            match[status].append(cve)
            if not flag:
                logger.debug(" no update need notify")
                return

            self.send_markdown(self.pretty_update_info(info_type, match), key=notifier_token)
            if 'data' in match:
                cve_data = data_to_csv(match['data'], column_list=self.get_info_column(info_type))
                self.send_file(cve_data, filename=f"{info_type}完整信息.csv", key=notifier_token)
            if "add" in match and len(match["add"]) > 0:
                add_cve_data = data_to_csv(match["add"], column_list=self.get_info_column(info_type))
                self.send_file(add_cve_data, filename=f"新增{info_type}信息.csv", key=notifier_token)
            if "modify" in match and len(match["modify"]) > 0:
                modify_cve_data = data_to_csv(match["modify"], column_list=self.get_info_column(info_type))
                self.send_file(modify_cve_data, filename=f"修改{info_type}信息.csv", key=notifier_token)
            if "delete" in match and len(match["delete"]) > 0:
                del_cve_data = data_to_csv(match["delete"], column_list=self.get_info_column(info_type))
                self.send_file(del_cve_data, filename=f"删减{info_type}信息.csv", key=notifier_token)

        logger.debug(f" * {self._name} do_update_notify done!")

    def do_status_notify(self, info_list: list, info_type: str):
        """
        状态播报
        :param info_list:
        :param info_type:
        :return:
        """
        for w in self._watcher:
            cw = self._watcher[w]
            if self._name not in cw:
                continue
            if len(cw[self._name]) < 2 and '' == cw[self._name][1]:
                continue

            notifier_token = cw[self._name][1]
            match = []
            flag = False

            filter_pkglist = cw['watch_pkg'] if 'watch_pkg' in cw else []
            if 'ALL' in filter_pkglist:
                match = info_list
                flag = True

            filter_score = cw['watch_score'] if 'watch_score' in cw else 0.0
            if filter_score <= 0.0:
                match = info_list
                flag = True

            if not match:
                for index, cve in enumerate(info_list):
                    if "packageName" in cve and "" != cve["packageName"]:
                        for fpkg in filter_pkglist:
                            if fpkg in filter_pkglist:
                                flag = True
                                match.append(cve)
                    elif "cvsssCoreNVD" in cve \
                            and "" != cve["cvsssCoreNVD"] \
                            and float(cve["cvsssCoreNVD"]) >= filter_score:
                        flag = True
                        match.append(cve)
                    elif "cvsssCoreOE" in cve \
                            and "" != cve["cvsssCoreOE"] \
                            and float(cve["cvsssCoreOE"]) >= filter_score:
                        flag = True
                        match.append(cve)

            logger.debug(
                f" * {self._name} do_status_nofity {w} info:{info_type} filter:{filter_pkglist} len:{len(match)}"
            )

            if flag and self._name in cw and 0 != len(match):
                info = self.pretty_status_info(info_type, match)
                self.send_markdown(msg=info, key=notifier_token)
                cve_data = data_to_csv(match, column_list=self.get_info_column(info_type))
                self.send_file(cve_data, key=notifier_token, filename=f"{info_type}信息列表.csv")
        logger.debug(f" * {self._name} do_status_notify done!")

    def pretty_status_info(self, info_type: str, list_info: list):
        """
        格式化状态播报信息，多态
        :param info_type:
        :param list_info:
        :return:
        """
        pretty_func_name = f"do_{info_type}_status_pretty"
        if super().hasFunction(pretty_func_name):
            return getattr(self, pretty_func_name)(list_info)
        else:
            logger.debug(f" * no func {pretty_func_name} found in {self.__class__.__name__}")
            return ""

    def pretty_update_info(self, info_type: str, dict_info: dict):
        """
        格式化更新播报信息，多态
        :param info_type:
        :param dict_info:
        :return:
        """
        pretty_func_name = f"do_{info_type}_update_pretty"
        if super().hasFunction(pretty_func_name):
            return getattr(self, pretty_func_name)(dict_info)
        else:
            logger.debug(f" * no func {pretty_func_name} found in {self.__class__.__name__}")
            return ""

    def per_cve_info_pretty(self, record):
        """
        每CVE信息格式化
        :param record:
        :return:
        """
        return (
                "`评分:[**<font color=\"red\">{}</font>**] {}`\n" +
                " 包名: {}\n" +
                " 发布时间: **{}**\n" +
                " [详情链接](https://www.openeuler.org/zh/security/cve/detail/?cveId={}&packageName={})\n"
        ).format(
            record["cvsssCoreNVD"] if record["cvsssCoreNVD"] else record["cvsssCoreOE"],
            record["cveId"],
            record["packageName"],
            record["updateTime"],
            record["cveId"],
            record["packageName"],
        )

    def do_cve_status_pretty(self, list_info: list):
        """
        CVE信息 状态播报 格式化
        :param list_info:
        :return:
        """
        message = (
                "### Msg from CVE-EASE: \n" +
                " CVE安全公告 - 状态播报\n" +
                " {}\n" +
                "服务状态: **<font color=\"info\">OK</font>**\n" +
                "服务IP: **<font color=\"info\">{}</font>**\n" +
                "CVE记录总数: **<font color=\"red\">{}</font>**\n" +
                "更新情况:\n" +
                "Top**{}**信息概况:\n" +
                "\n"
        ).format(
            self._notify_time,
            self._wanip,
            len(list_info),
            self._config.NOTIFIER_RECORD_NUM
        )
        count = int(self._config.NOTIFIER_RECORD_NUM)
        for r in list_info[:count]:
            message += self.per_cve_info_pretty(r)

        message += """\n**相关同事请务必及时修复漏洞，杜绝隐患保障系统安全。**\n"""
        return message

    def do_cve_update_pretty(self, dict_info: dict):
        """
        CVE信息 更新播报 格式化
        :param dict_info:
        :return:
        """
        message = (
                "### Msg from CVE-EASE: \n" +
                " CVE安全公告 - 更新播报\n" +
                " {}\n" +
                "服务状态: **<font color=\"info\">OK</font>**\n" +
                "服务IP: **<font color=\"info\">{}</font>**\n" +
                "CVE记录总数: **<font color=\"red\">{}</font>**\n" +
                "更新情况:\n" +
                " 新增:**<font color=\"red\">{}</font>**" +
                " 修改:**<font color=\"red\">{}</font>**" +
                " 删减:**<font color=\"red\">{}</font>**\n" +
                "Top**{}**变更信息概况:\n" +
                "\n"
        ).format(
            notifier_timestamp(),
            self._wanip,
            len(dict_info["data"]),
            len(dict_info["add"]),
            len(dict_info["modify"]),
            len(dict_info["delete"]),
            self._config.NOTIFIER_RECORD_NUM
        )
        count = int(self._config.NOTIFIER_RECORD_NUM)
        addlist = sorted(dict_info["add"], key=lambda d: d['id'])
        modifylist = sorted(dict_info["modify"], key=lambda d: d['id'])
        dellist = sorted(dict_info["delete"], key=lambda d: d['id'])

        need_output_record_list = addlist + modifylist + dellist
        for r in need_output_record_list[:count]:
            message += self.per_cve_info_pretty(r)

        message += """\n**相关同事请务必及时修复漏洞，杜绝隐患保障系统安全。**\n"""
        return message

    def per_sa_info_pretty(self, record):
        """
        每SA信息 格式化
        :param record:
        :return:
        """
        return (
                "`编号:{}`\n" +
                " 包名: **{}**\n" +
                " CVEID: {}\n" +
                " 发布时间: **{}**\n" +
                " [详情链接](https://www.openeuler.org/zh/security/safety-bulletin/detail/?id={})\n"
        ).format(
            record["securityNoticeNo"],
            record["affectedComponent"],
            record["cveId"],
            record["updateTime"],
            record["securityNoticeNo"],
        )

    def do_sa_status_pretty(self, list_info: list):
        """
        SA信息 状态播报格式化
        :param list_info:
        :return:
        """
        message = (
                "### Msg from CVE-EASE: \n" +
                " SA安全公告 - 状态播报\n" +
                " {}\n" +
                "服务状态: **<font color=\"info\">OK</font>**\n" +
                "服务IP: **<font color=\"info\">{}</font>**\n" +
                "SA记录总数: **<font color=\"red\">{}</font>**\n" +
                "更新情况:\n" +
                "Top**{}**信息概况:\n" +
                "\n"
        ).format(
            self._notify_time,
            self._wanip,
            len(list_info),
            self._config.NOTIFIER_RECORD_NUM
        )
        count = int(self._config.NOTIFIER_RECORD_NUM)
        for r in list_info[:count]:
            message += self.per_sa_info_pretty(r)

        message += """\n**相关同事请务必及时修复漏洞，杜绝隐患保障系统安全。**\n"""
        return message

    def do_sa_update_pretty(self, dict_info: dict):
        """
        SA信息 更新播报 格式化
        :param dict_info:
        :return:
        """
        message = (
                "### Msg from CVE-EASE: \n" +
                " SA安全公告 - 更新播报\n" +
                " {}\n" +
                "服务状态: **<font color=\"info\">OK</font>**\n" +
                "服务IP: **<font color=\"info\">{}</font>**\n" +
                "SA记录总数: **<font color=\"red\">{}</font>**\n" +
                "更新情况:\n" +
                " 新增:**<font color=\"red\">{}</font>**" +
                " 修改:**<font color=\"red\">{}</font>**" +
                " 删减:**<font color=\"red\">{}</font>**\n" +
                "Top**{}**变更信息概况:\n" +
                "\n"
        ).format(
            self._notify_time,
            self._wanip,
            len(dict_info["data"]),
            len(dict_info["add"]),
            len(dict_info["modify"]),
            len(dict_info["delete"]),
            self._config.NOTIFIER_RECORD_NUM
        )
        count = int(self._config.NOTIFIER_RECORD_NUM)
        addlist = sorted(dict_info["add"], key=lambda d: d['id'])
        modifylist = sorted(dict_info["modify"], key=lambda d: d['id'])
        dellist = sorted(dict_info["delete"], key=lambda d: d['id'])

        need_output_record_list = addlist + modifylist + dellist
        for r in need_output_record_list[:count]:
            message += self.per_sa_info_pretty(r)

        message += """\n**相关同事请务必及时修复漏洞，杜绝隐患保障系统安全。**\n"""
        return message
